package webhook

import (
	"maps"
	"path"
	"slices"

	policiesv1beta1 "github.com/kyverno/kyverno/api/policies.kyverno.io/v1beta1"
	"github.com/kyverno/kyverno/pkg/cel/autogen"
	ivpolautogen "github.com/kyverno/kyverno/pkg/cel/policies/ivpol/autogen"
	mpolautogen "github.com/kyverno/kyverno/pkg/cel/policies/mpol/autogen"
	vpolautogen "github.com/kyverno/kyverno/pkg/cel/policies/vpol/autogen"
	"github.com/kyverno/kyverno/pkg/config"
	engineapi "github.com/kyverno/kyverno/pkg/engine/api"
	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
)

func buildWebhookRules(cfg config.Configuration, server, name, queryPath string, servicePort int32, caBundle []byte, policies []engineapi.GenericPolicy, expressionCache *expressionCache) []admissionregistrationv1.ValidatingWebhook {
	var fineGrained, basic []engineapi.GenericPolicy
	for _, policy := range policies {
		p := extractGenericPolicy(policy)
		if validConditions(expressionCache, p.GetMatchConditions()) != nil {
			fineGrained = append(fineGrained, policy)
		} else if p.GetMatchConstraints().MatchPolicy != nil && *p.GetMatchConstraints().MatchPolicy == admissionregistrationv1.Exact {
			fineGrained = append(fineGrained, policy)
		} else if p.GetTimeoutSeconds() != nil {
			fineGrained = append(fineGrained, policy)
		} else {
			basic = append(basic, policy)
		}
	}
	var webhooks []admissionregistrationv1.ValidatingWebhook
	// process fine grained policies
	if len(fineGrained) != 0 {
		var fineGrainedIgnoreList, fineGrainedFailList []admissionregistrationv1.ValidatingWebhook
		for _, policy := range fineGrained {
			p := extractGenericPolicy(policy)
			webhook := admissionregistrationv1.ValidatingWebhook{
				SideEffects:             &noneOnDryRun,
				AdmissionReviewVersions: []string{"v1"},
			}
			if ok := autogen.CanAutoGen(ptr.To(p.GetMatchConstraints())); ok {
				webhook.MatchConditions = append(
					webhook.MatchConditions,
					autogen.CreateMatchConditions(
						"",
						[]policiesv1beta1.Target{{
							Group:    "",
							Version:  "v1",
							Resource: "pods",
							Kind:     "Pod",
						}},
						validConditions(expressionCache, p.GetMatchConditions()),
					)...,
				)
			} else {
				webhook.MatchConditions = append(webhook.MatchConditions, validConditions(expressionCache, p.GetMatchConditions())...)
			}

			if _, ok := p.(*policiesv1beta1.GeneratingPolicy); ok {
				// all four operations including CONNECT are needed for generate.
				for _, match := range p.GetMatchConstraints().ResourceRules {
					rule := match.RuleWithOperations
					rule.Operations = []admissionregistrationv1.OperationType{
						admissionregistrationv1.Create,
						admissionregistrationv1.Update,
						admissionregistrationv1.Delete,
						admissionregistrationv1.Connect,
					}
					webhook.Rules = append(webhook.Rules, rule)
				}
			} else {
				for _, match := range p.GetMatchConstraints().ResourceRules {
					webhook.Rules = append(webhook.Rules, match.RuleWithOperations)
				}
			}
			if vpol, ok := p.(*policiesv1beta1.ValidatingPolicy); ok {
				policies, err := vpolautogen.Autogen(vpol)
				if err != nil {
					continue
				}
				for _, config := range slices.Sorted(maps.Keys(policies)) {
					policy := policies[config]
					webhook.MatchConditions = append(
						webhook.MatchConditions,
						autogen.CreateMatchConditions(config, policy.Targets, validConditions(expressionCache, policy.Spec.MatchConditions))...,
					)
					for _, match := range policy.Spec.MatchConstraints.ResourceRules {
						webhook.Rules = append(webhook.Rules, match.RuleWithOperations)
					}
				}
			}
			if nvpol, ok := p.(*policiesv1beta1.NamespacedValidatingPolicy); ok {
				policies, err := vpolautogen.Autogen(nvpol)
				if err != nil {
					continue
				}
				for _, config := range slices.Sorted(maps.Keys(policies)) {
					policy := policies[config]
					webhook.MatchConditions = append(
						webhook.MatchConditions,
						autogen.CreateMatchConditions(config, policy.Targets, validConditions(expressionCache, policy.Spec.MatchConditions))...,
					)
					for _, match := range policy.Spec.MatchConstraints.ResourceRules {
						webhook.Rules = append(webhook.Rules, match.RuleWithOperations)
					}
				}
			}
			if ivpol, ok := p.(*policiesv1beta1.ImageValidatingPolicy); ok {
				policies, err := ivpolautogen.Autogen(ivpol)
				if err != nil {
					continue
				}
				for _, config := range slices.Sorted(maps.Keys(policies)) {
					policy := policies[config]
					webhook.MatchConditions = append(
						webhook.MatchConditions,
						autogen.CreateMatchConditions(config, policy.Targets, validConditions(expressionCache, policy.Spec.MatchConditions))...,
					)
					for _, match := range policy.Spec.MatchConstraints.ResourceRules {
						webhook.Rules = append(webhook.Rules, match.RuleWithOperations)
					}
				}
			}

			if nivpol, ok := p.(*policiesv1beta1.NamespacedImageValidatingPolicy); ok {
				policies, err := ivpolautogen.AutogenNamespaced(nivpol)
				if err != nil {
					continue
				}
				for _, config := range slices.Sorted(maps.Keys(policies)) {
					policy := policies[config]
					webhook.MatchConditions = append(
						webhook.MatchConditions,
						autogen.CreateMatchConditions(config, policy.Targets, validConditions(expressionCache, policy.Spec.MatchConditions))...,
					)
					for _, match := range policy.Spec.MatchConstraints.ResourceRules {
						webhook.Rules = append(webhook.Rules, match.RuleWithOperations)
					}
				}
			}

			if mpol := policy.AsMutatingPolicy(); mpol != nil {
				policies, err := mpolautogen.Autogen(mpol)
				if err != nil {
					logger.Error(err, "failed to auto-generate mutating policy", "policy", mpol.GetName())
					continue
				}
				for _, config := range slices.Sorted(maps.Keys(policies)) {
					autogenPolicy := policies[config]
					webhook.MatchConditions = append(
						webhook.MatchConditions,
						autogen.CreateMatchConditions(config, autogenPolicy.Targets, validConditions(expressionCache, autogenPolicy.Spec.MatchConditions))...,
					)
					for _, match := range autogenPolicy.Spec.MatchConstraints.ResourceRules {
						webhook.Rules = append(webhook.Rules, match.RuleWithOperations)
					}
				}
			}

			if nmpol := policy.AsNamespacedMutatingPolicy(); nmpol != nil {
				policies, err := mpolautogen.Autogen(nmpol)
				if err != nil {
					continue
				}
				for _, config := range slices.Sorted(maps.Keys(policies)) {
					autogenPolicy := policies[config]
					webhook.MatchConditions = append(
						webhook.MatchConditions,
						autogen.CreateMatchConditions(config, autogenPolicy.Targets, validConditions(expressionCache, autogenPolicy.Spec.MatchConditions))...,
					)
					for _, match := range autogenPolicy.Spec.MatchConstraints.ResourceRules {
						webhook.Rules = append(webhook.Rules, match.RuleWithOperations)
					}
				}
			}

			if p.GetMatchConstraints().MatchPolicy != nil && *p.GetMatchConstraints().MatchPolicy == admissionregistrationv1.Exact {
				webhook.MatchPolicy = p.GetMatchConstraints().MatchPolicy
			}
			if p.GetTimeoutSeconds() != nil {
				webhook.TimeoutSeconds = p.GetTimeoutSeconds()
			}
			if p.GetFailurePolicy() == admissionregistrationv1.Ignore {
				webhook.FailurePolicy = ptr.To(admissionregistrationv1.Ignore)
				webhook.Name = name + "-ignore-finegrained-" + p.GetName()
				webhook.ClientConfig = newClientConfig(server, servicePort, caBundle, path.Join(queryPath, p.GetName()))
				webhook.NamespaceSelector = mergeLabelSelectors(
					p.GetMatchConstraints().NamespaceSelector,
					cfg.GetWebhook().NamespaceSelector,
				)
				webhook.ObjectSelector = mergeLabelSelectors(
					p.GetMatchConstraints().ObjectSelector,
					cfg.GetWebhook().ObjectSelector,
				)
				fineGrainedIgnoreList = append(fineGrainedIgnoreList, webhook)
			} else {
				webhook.FailurePolicy = ptr.To(admissionregistrationv1.Fail)
				webhook.Name = name + "-fail-finegrained-" + p.GetName()
				webhook.ClientConfig = newClientConfig(server, servicePort, caBundle, path.Join(queryPath, p.GetName()))
				webhook.NamespaceSelector = mergeLabelSelectors(
					p.GetMatchConstraints().NamespaceSelector,
					cfg.GetWebhook().NamespaceSelector,
				)
				webhook.ObjectSelector = mergeLabelSelectors(
					p.GetMatchConstraints().ObjectSelector,
					cfg.GetWebhook().ObjectSelector,
				)
				fineGrainedFailList = append(fineGrainedFailList, webhook)
			}
		}
		if fineGrainedFailList != nil {
			webhooks = append(webhooks, fineGrainedFailList...)
		}
		if fineGrainedIgnoreList != nil {
			webhooks = append(webhooks, fineGrainedIgnoreList...)
		}
	}
	// process basic policies
	if len(basic) != 0 {
		names := make([]string, 0, len(basic))
		for _, policy := range basic {
			names = append(names, policy.GetName())
		}
		slices.Sort(names)
		dynamicPath := path.Join(names...)
		webhookIgnore := admissionregistrationv1.ValidatingWebhook{
			Name:                    name + "-ignore",
			ClientConfig:            newClientConfig(server, servicePort, caBundle, path.Join(queryPath, dynamicPath)),
			FailurePolicy:           ptr.To(admissionregistrationv1.Ignore),
			SideEffects:             &noneOnDryRun,
			AdmissionReviewVersions: []string{"v1"},
		}
		webhookFail := admissionregistrationv1.ValidatingWebhook{
			Name:                    name + "-fail",
			ClientConfig:            newClientConfig(server, servicePort, caBundle, path.Join(queryPath, dynamicPath)),
			FailurePolicy:           ptr.To(admissionregistrationv1.Fail),
			SideEffects:             &noneOnDryRun,
			AdmissionReviewVersions: []string{"v1"},
		}
		webhookFail.NamespaceSelector = cfg.GetWebhook().NamespaceSelector

		for _, policy := range basic {
			p := extractGenericPolicy(policy)
			webhookIgnore.NamespaceSelector = mergeLabelSelectors(
				p.GetMatchConstraints().NamespaceSelector,
				cfg.GetWebhook().NamespaceSelector,
			)
			webhookIgnore.ObjectSelector = mergeLabelSelectors(
				p.GetMatchConstraints().ObjectSelector,
				cfg.GetWebhook().ObjectSelector,
			)
			webhookFail.NamespaceSelector = mergeLabelSelectors(
				p.GetMatchConstraints().NamespaceSelector,
				cfg.GetWebhook().NamespaceSelector,
			)
			webhookFail.ObjectSelector = mergeLabelSelectors(
				p.GetMatchConstraints().ObjectSelector,
				cfg.GetWebhook().ObjectSelector,
			)
			var webhookRules []admissionregistrationv1.RuleWithOperations
			if vpol, ok := p.(*policiesv1beta1.ValidatingPolicy); ok {
				rules, err := vpolautogen.Autogen(vpol)
				if err != nil {
					continue
				}
				for _, rule := range rules {
					for _, match := range rule.Spec.MatchConstraints.ResourceRules {
						webhookRules = append(webhookRules, match.RuleWithOperations)
					}
				}
			}
			if ivpol, ok := p.(*policiesv1beta1.ImageValidatingPolicy); ok {
				autogeneratedIvPols, err := ivpolautogen.Autogen(ivpol)
				if err != nil {
					continue
				}
				for _, p := range autogeneratedIvPols {
					for _, match := range p.Spec.MatchConstraints.ResourceRules {
						webhookRules = append(webhookRules, match.RuleWithOperations)
					}
				}
			}
			if nivpol, ok := p.(*policiesv1beta1.NamespacedImageValidatingPolicy); ok {
				autogeneratedNivPols, err := ivpolautogen.AutogenNamespaced(nivpol)
				if err != nil {
					continue
				}
				for _, p := range autogeneratedNivPols {
					for _, match := range p.Spec.MatchConstraints.ResourceRules {
						webhookRules = append(webhookRules, match.RuleWithOperations)
					}
				}
			}
			if mpol := policy.AsMutatingPolicy(); mpol != nil {
				rules, err := mpolautogen.Autogen(mpol)
				if err != nil {
					continue
				}
				for _, rule := range rules {
					for _, match := range rule.Spec.MatchConstraints.ResourceRules {
						webhookRules = append(webhookRules, match.RuleWithOperations)
					}
				}
			}
			if nmpol := policy.AsNamespacedMutatingPolicy(); nmpol != nil {
				rules, err := mpolautogen.Autogen(nmpol)
				if err != nil {
					continue
				}
				for _, rule := range rules {
					for _, match := range rule.Spec.MatchConstraints.ResourceRules {
						webhookRules = append(webhookRules, match.RuleWithOperations)
					}
				}
			}
			if _, ok := p.(*policiesv1beta1.GeneratingPolicy); ok {
				// all four operations including CONNECT are needed for generate.
				for _, match := range p.GetMatchConstraints().ResourceRules {
					rule := match.RuleWithOperations
					rule.Operations = []admissionregistrationv1.OperationType{
						admissionregistrationv1.Create,
						admissionregistrationv1.Update,
						admissionregistrationv1.Delete,
						admissionregistrationv1.Connect,
					}
					webhookRules = append(webhookRules, rule)
				}
			} else {
				for _, match := range p.GetMatchConstraints().ResourceRules {
					webhookRules = append(webhookRules, match.RuleWithOperations)
				}
			}
			if p.GetFailurePolicy() == admissionregistrationv1.Ignore {
				webhookIgnore.Rules = append(webhookIgnore.Rules, webhookRules...)
			} else {
				webhookFail.Rules = append(webhookFail.Rules, webhookRules...)
			}
		}
		if webhookFail.Rules != nil {
			webhooks = append(webhooks, webhookFail)
		}
		if webhookIgnore.Rules != nil {
			webhooks = append(webhooks, webhookIgnore)
		}
	}
	return webhooks
}

func mergeLabelSelectors(a, b *metav1.LabelSelector) *metav1.LabelSelector {
	if a == nil {
		return b
	}
	if b == nil {
		return a
	}

	merged := &metav1.LabelSelector{
		MatchLabels:      map[string]string{},
		MatchExpressions: []metav1.LabelSelectorRequirement{},
	}

	// copy a
	for k, v := range a.MatchLabels {
		merged.MatchLabels[k] = v
	}
	merged.MatchExpressions = append(merged.MatchExpressions, a.MatchExpressions...)

	// copy b
	for k, v := range b.MatchLabels {
		merged.MatchLabels[k] = v
	}
	merged.MatchExpressions = append(merged.MatchExpressions, b.MatchExpressions...)

	return merged
}

func validConditions(celExpressionCache *expressionCache, conditions []admissionregistrationv1.MatchCondition) []admissionregistrationv1.MatchCondition {
	if celExpressionCache == nil {
		return nil
	}
	valid, err := celExpressionCache.ValidateMatchConditions(conditions)
	if err != nil {
		logger.V(6).Info("skip building the webhook with Kubernetes unknown match conditions", "error", err.ToAggregate().Error())
	}
	if len(valid) == len(conditions) {
		return conditions
	}
	return nil
}
